package jxtend.processor.eclipse.visitor.handler.impl;

import jxtend.annotation.keyword.equalsAndHashCode;
import jxtend.processor.eclipse.utils.ASTNodeUtils;
import jxtend.processor.eclipse.utils.EclipseProcessorUtils;
import jxtend.processor.eclipse.visitor.handler.ASTNodeHandlerVisitorContext;
import jxtend.processor.eclipse.visitor.handler.AbstractASTNodeVisitorHandler;

import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.internal.compiler.ast.AND_AND_Expression;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.ArrayAllocationExpression;
import org.eclipse.jdt.internal.compiler.ast.ArrayInitializer;
import org.eclipse.jdt.internal.compiler.ast.Block;
import org.eclipse.jdt.internal.compiler.ast.CastExpression;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FalseLiteral;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.FieldReference;
import org.eclipse.jdt.internal.compiler.ast.IfStatement;
import org.eclipse.jdt.internal.compiler.ast.InstanceOfExpression;
import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration;
import org.eclipse.jdt.internal.compiler.ast.MessageSend;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.NameReference;
import org.eclipse.jdt.internal.compiler.ast.OperatorIds;
import org.eclipse.jdt.internal.compiler.ast.QualifiedNameReference;
import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ReturnStatement;
import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.Statement;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.ast.UnaryExpression;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.lookup.ExtraCompilerModifiers;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;

@SuppressWarnings("restriction")
public class EqualsAndHashCodeHandler extends AbstractASTNodeVisitorHandler {

	@Override
	public void beginVisit(ASTNodeHandlerVisitorContext context, ASTNode node, Scope scope) {
		if (!(node instanceof QualifiedTypeReference)
			    || !EclipseProcessorUtils.compoundNameEquals(
			    		((QualifiedTypeReference) node).tokens, equalsAndHashCode.class)
				|| !(context.getParentNode(0) instanceof Annotation)
				|| !(context.getParentNode(1) instanceof TypeDeclaration)) {
			return;
		}

		TypeDeclaration typeDeclaration = (TypeDeclaration) context.getParentNode(1);
		Annotation equalsAndHashCodeAnnotation = (Annotation) context.getParentNode(0);

		appendEqualsMethod(context, typeDeclaration, equalsAndHashCodeAnnotation, scope);
		appendHashCodeMethod(context, typeDeclaration, equalsAndHashCodeAnnotation, scope);
	}

	private static void appendEqualsMethod(ASTNodeHandlerVisitorContext context, TypeDeclaration typeDeclaration, 
			Annotation equalsAndHashCodeAnnotation, Scope scope) {
		if ((typeDeclaration.fields == null) || (typeDeclaration.fields.length == 0)) {
			return;
		}

		long annotationPos = encodeSourceStartAndEndPosition(equalsAndHashCodeAnnotation);

		MethodDeclaration methodDeclaration = new MethodDeclaration(typeDeclaration.compilationResult);
		methodDeclaration.modifiers = ClassFileConstants.AccPublic;
		methodDeclaration.returnType = TypeReference.baseTypeReference(TypeIds.T_boolean, 0);
		methodDeclaration.selector = "equals".toCharArray();

		methodDeclaration.declarationSourceStart =
			methodDeclaration.sourceStart =
				methodDeclaration.modifiersSourceStart =
					methodDeclaration.bodyStart =
						equalsAndHashCodeAnnotation.sourceStart;
		methodDeclaration.declarationSourceEnd =
			methodDeclaration.sourceEnd =
				equalsAndHashCodeAnnotation.sourceEnd;

		Argument argument = new Argument("obj".toCharArray(), annotationPos,
				new QualifiedTypeReference(
						TypeConstants.JAVA_LANG_OBJECT, ASTNodeUtils.encodePosArray(3, annotationPos)), Modifier.FINAL);
		argument.declarationSourceStart =
			argument.sourceStart =
				argument.modifiersSourceStart =
					equalsAndHashCodeAnnotation.sourceStart;
		argument.declarationSourceEnd =
			argument.sourceEnd =
				equalsAndHashCodeAnnotation.sourceEnd;
		methodDeclaration.arguments = new Argument[] { argument };

		methodDeclaration.annotations = new Annotation[] { OverrideAnnotationUtils.createJavaLangOverrideAnnotation(
				context, equalsAndHashCodeAnnotation.sourceStart, equalsAndHashCodeAnnotation.sourceEnd, scope) };
		
		TypeReference typeReference = new SingleTypeReference(typeDeclaration.name, annotationPos);
		NameReference paramReference = new SingleNameReference(argument.name, annotationPos);

		InstanceOfExpression instanceOfExpression = new InstanceOfExpression(paramReference, typeReference);
		UnaryExpression notInstanceOfExpressionExpression = new UnaryExpression(instanceOfExpression, OperatorIds.NOT);

		Expression falseLiteralExpression =
			new FalseLiteral(equalsAndHashCodeAnnotation.sourceStart, equalsAndHashCodeAnnotation.sourceEnd);
		Statement returnFalseStatement = new ReturnStatement(falseLiteralExpression,
				equalsAndHashCodeAnnotation.sourceStart, equalsAndHashCodeAnnotation.sourceEnd);

		Block thenStatement = new Block(0);
		thenStatement.statements = new Statement[] { returnFalseStatement };

		IfStatement ifNotInstanceOfTypeStatement = new IfStatement(notInstanceOfExpressionExpression, thenStatement,
				equalsAndHashCodeAnnotation.sourceStart, equalsAndHashCodeAnnotation.sourceEnd);

		LocalDeclaration rhsVariableDeclaration = new LocalDeclaration("rhs".toCharArray(),
				equalsAndHashCodeAnnotation.sourceStart, equalsAndHashCodeAnnotation.sourceEnd);
		rhsVariableDeclaration.type = new SingleTypeReference(typeDeclaration.name, annotationPos);
		rhsVariableDeclaration.initialization = new CastExpression(
				new SingleNameReference(argument.name, annotationPos), (TypeReference) rhsVariableDeclaration.type);

		Expression fieldEqualExpression = null;

		for (FieldDeclaration fieldDeclaration : typeDeclaration.fields) {
			FieldReference fieldReferenceOnRhsVariable = new FieldReference(fieldDeclaration.name, annotationPos);
			fieldReferenceOnRhsVariable.receiver = new SingleNameReference(rhsVariableDeclaration.name, annotationPos);

			MessageSend messageSend = new MessageSend();
			messageSend.receiver = new QualifiedNameReference(JxtendTypeConstants.QUALIFIED_JXTEND_INTERNAL_UTILS,
					ASTNodeUtils.encodePosArray(
							JxtendTypeConstants.QUALIFIED_JXTEND_INTERNAL_UTILS.length, annotationPos),
					equalsAndHashCodeAnnotation.sourceStart, equalsAndHashCodeAnnotation.sourceEnd);
			messageSend.selector = "equal".toCharArray();
			messageSend.arguments = new Expression[] {
					new SingleNameReference(fieldDeclaration.name, annotationPos), fieldReferenceOnRhsVariable };

			if (fieldEqualExpression != null) {
				fieldEqualExpression = new AND_AND_Expression(fieldEqualExpression, messageSend, OperatorIds.AND_AND);
				fieldEqualExpression.sourceStart = messageSend.sourceStart;
				fieldEqualExpression.statementEnd =
					fieldEqualExpression.sourceEnd =
						messageSend.sourceEnd;
			} else {
				fieldEqualExpression = messageSend;
			}
		}

		Statement returnFieldEqualExpression = new ReturnStatement(fieldEqualExpression,
				equalsAndHashCodeAnnotation.sourceStart, equalsAndHashCodeAnnotation.sourceEnd);

		methodDeclaration.statements =
			new Statement[] { ifNotInstanceOfTypeStatement, rhsVariableDeclaration, returnFieldEqualExpression };

		context.appendMethod(typeDeclaration, methodDeclaration);
		methodDeclaration.binding.tagBits |= ExtraCompilerModifiers.AccOverriding;
	}

	private static void appendHashCodeMethod(ASTNodeHandlerVisitorContext context, TypeDeclaration typeDeclaration, 
			Annotation equalsAndHashCodeAnnotation, Scope scope) {
		if ((typeDeclaration.fields == null) || (typeDeclaration.fields.length == 0)) {
			return;
		}

		long annotationPos = encodeSourceStartAndEndPosition(equalsAndHashCodeAnnotation);

		MethodDeclaration methodDeclaration = new MethodDeclaration(typeDeclaration.compilationResult);
		methodDeclaration.modifiers = ClassFileConstants.AccPublic;
		methodDeclaration.returnType = TypeReference.baseTypeReference(TypeIds.T_int, 0);
		methodDeclaration.selector = "hashCode".toCharArray();

		methodDeclaration.declarationSourceStart =
			methodDeclaration.sourceStart =
			methodDeclaration.modifiersSourceStart =
				methodDeclaration.bodyStart =
					equalsAndHashCodeAnnotation.sourceStart;
		methodDeclaration.declarationSourceEnd =
			methodDeclaration.sourceEnd =
				equalsAndHashCodeAnnotation.sourceEnd;

		methodDeclaration.annotations = new Annotation[] { OverrideAnnotationUtils.createJavaLangOverrideAnnotation(
				context, equalsAndHashCodeAnnotation.sourceStart, equalsAndHashCodeAnnotation.sourceEnd, scope) };

		ArrayAllocationExpression arrayAllocationExpression = new ArrayAllocationExpression();
		arrayAllocationExpression.sourceStart = equalsAndHashCodeAnnotation.sourceStart;
		arrayAllocationExpression.statementEnd =
			arrayAllocationExpression.sourceEnd =
				equalsAndHashCodeAnnotation.sourceEnd;
		arrayAllocationExpression.type = new QualifiedTypeReference(TypeConstants.JAVA_LANG_OBJECT,
				ASTNodeUtils.encodePosArray(TypeConstants.JAVA_LANG_OBJECT.length, annotationPos));
		arrayAllocationExpression.dimensions = new Expression[1];
		arrayAllocationExpression.initializer = new ArrayInitializer();
		arrayAllocationExpression.initializer.expressions = new Expression[typeDeclaration.fields.length];
		arrayAllocationExpression.initializer.sourceStart = equalsAndHashCodeAnnotation.sourceStart;
		arrayAllocationExpression.initializer.statementEnd =
			arrayAllocationExpression.initializer.sourceEnd =
				equalsAndHashCodeAnnotation.sourceEnd;

		for (int i = 0; i < typeDeclaration.fields.length; i++) {
			arrayAllocationExpression.initializer.expressions[i] =
				new SingleNameReference(typeDeclaration.fields[i].name, annotationPos);
		}

		MessageSend messageSend = new MessageSend();
		messageSend.receiver = new QualifiedNameReference(JxtendTypeConstants.JAVA_UTIL_ARRAYS,
				ASTNodeUtils.encodePosArray(JxtendTypeConstants.JAVA_UTIL_ARRAYS.length, annotationPos),
				equalsAndHashCodeAnnotation.sourceStart, equalsAndHashCodeAnnotation.sourceEnd);
		messageSend.selector = "hashCode".toCharArray();
		messageSend.arguments = new Expression[] { arrayAllocationExpression };

		Statement returnArraysHashCodeOfFields = new ReturnStatement(messageSend,
				equalsAndHashCodeAnnotation.sourceStart, equalsAndHashCodeAnnotation.sourceEnd);

		methodDeclaration.statements = new Statement[] { returnArraysHashCodeOfFields };

		context.appendMethod(typeDeclaration, methodDeclaration);
		methodDeclaration.binding.tagBits |= ExtraCompilerModifiers.AccOverriding;
	}

	private static long encodeSourceStartAndEndPosition(ASTNode astNode) {
		int sourceStart = astNode.sourceStart;
		int sourceEnd = astNode.sourceEnd;
		return ((long) sourceStart << 32) | sourceEnd;
	}
}
